/* License Notice:
**
** This program is free software: you can redistribute it and/or modify
**    it under the terms of the GNU General Public License as published by
**    the Free Software Foundation, either version 3 of the License, or
**    (at your option) any later version.
** This program is distributed in the hope that it will be useful,
**   but WITHOUT ANY WARRANTY; without even the implied warranty of
**   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
**   GNU General Public License for more details.
** You should have received a copy of the GNU General Public License
**   along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

/**
 * @file resource_manager.cpp
 * @author TooOld2Rock'nRoll
 * @see resource_manager.hpp
 */
/*---- Includes ----*/
#include "resource_manager.hpp"

#include "toolbox/debug.h"


/*---- Global Variables ----*/
///A way to pack our Vertex Shader with the library file.
const std::string vert_shader =
#include "graphics/shaders/shader.vert.hpp"
;

///A way to pack our Fragment Shader with the library file.
const std::string frag_shader =
#include "graphics/shaders/shader.frag.hpp"
;


/*---- Methods Implementation ----*/
/**
 * @brief Base Constructor.
 * @remark Hiden to make singleton safe.
 */
ResourceManager::ResourceManager ()
{
    _debug (INFO, "::%s () <--The ResourceManager singleton has been created!", __FUNCTION__);

    this->_last_generated_id = 0;
}//End Constructor


/**
 * @brief Access the singleton instance.
 *
 * @return Reference to the ResourceManager singleton.
 */
ResourceManager& ResourceManager::instance ()
{
    static ResourceManager _instance;

    _debug (TRACE, "::%s ()", __FUNCTION__);

    return _instance;
}//End instance ()


/**
 * @brief Destructor.
 */
ResourceManager::~ResourceManager ()
{
    _debug (INFO, "::%s () <--The ResourceManager singleton will be destroyed!", __FUNCTION__);

    this->map_domains.clear ();

    for (auto itr : this->map_resources)
        this->_eraseResource (itr.first);
    this->map_resources.clear ();
}//End Destructor


//decrements the reference count and deletes the resource if no one is using it anymore
void ResourceManager::_eraseResource (const std::string &key)
{
    switch (this->map_resources[key]._type)
    {
        case resource_type::SHADER:
            delete ((Shader *)this->map_resources[key].vp_package);
            break;
        //end case resource_type::SHADER:

        case resource_type::CAMERA:
            delete ((Camera2D *)this->map_resources[key].vp_package);
            break;
        //end case resource_type::CAMERA:

        case resource_type::TEXTURE:
            delete ((Texture *)this->map_resources[key].vp_package);
            break;
        //end case resource_type::TEXTURE:

        case resource_type::FONT:
            delete ((Font *)this->map_resources[key].vp_package);
            break;
        //end case resource_type::FONT:

        case resource_type::MUSIC:
            delete ((UndecodedFileSource *)this->map_resources[key].vp_package);
            break;
        //end case resource_type::MUSIC:

        case resource_type::SOUND_EFFECT:
            delete ((DecodedFileSource *)this->map_resources[key].vp_package);
            break;
        //end case resource_type::SOUND_EFFECT:

        //ERROR! we shouldn't be here
        default:
            _debug (ERROR, "We don't know this resource type %u, can't delete!!", this->map_resources[key]._type);
            break;
    }//end switch (this->map_resources[key]._type)
}//end _eraseResource ()


/**
 * @brief Returns a unique Resource Domain Identification that will be valid during the lifetime of the program.
 * @remark Altough the class will release any remaining resource once the singleton is destroied, it is a nice practice
 *            to free any domain that has outlasted its usefulness.
 *
 * @return A unique identifier to access the class services.
 *
 * @throws std::overflow_error - in case the id counter overflows and we can´t create more domains (it should never happen though).
 */
DomainID_t ResourceManager::createNewDomain ()
{
    _debug (TRACE, "::%s ()", __FUNCTION__);

    //check overflow
    if (this->_last_generated_id + 1 < this->_last_generated_id)
        throw std::overflow_error (fmt::format ("{0}:{1} - id generations has overflown!", __LINE__, __FILE__));

    ++this->_last_generated_id;

    return this->_last_generated_id;
}//End createNewDomain ()


/**
 * @brief Releases all resources related to the specific domain.
 * @remark If a resource is shared with other domains, only the reference to desired domain will be dropped, the resource
 *            will still be valid to all the others.
 *
 * @param id - the Resource Domain ID of the resource domain to be released.
 */
void ResourceManager::dropDomain (DomainID_t id)
{
    auto range = this->map_domains.equal_range (id);

    _debug (TRACE, "::%s (id=%u)", __FUNCTION__, id);

    //find the resources referenced and decrements its counter
    for (auto itr = range.first; itr != range.second; )
    {
        --this->map_resources[itr->second].u_count;

        if (this->map_resources[itr->second].u_count == 0)
        {
            this->_eraseResource (itr->second);
            this->map_resources.erase (itr->second);
        }//end if (this->map_resources[key].u_count == 0)

        //remove entry
        itr = this->map_domains.erase (itr);
    }//end for (auto itr = range.first; itr != range.second; ++itr)
}//End dropDomain ()


/**
 * @brief Gets the desired resource, allocates new instance if it doesn't exist yet.
 *
 * @param id - the domain id to be associated with this resource.
 * @param key - whatever it's used to access the resource, generally a file path.
 *
 * @return A pointer to the desired resource.
 *
 * @throws std::invalid_argument - in case key is null.
 * @throws std::runtime_error - in case there is any problem loading the resource file.
 * @throws std::overflow_error - in case the reference counter overflows and we can´t add more to domains (it should never happen though).
 */
void* ResourceManager::_getResource (DomainID_t id, const char *key, resource_type type)
{
    void *ret = nullptr;

    _debug (TRACE, "::%s (id=%u, key=%s, type=%u)", __FUNCTION__, id, key, type);

    if (!key)
        throw std::invalid_argument (fmt::format ("{0}:{1} - no file name provided!", __LINE__, __FILE__));

    //We can't use map.contains() for now, so this method helps retrieving the content of a map slot if it exists.
    //change it when upgrading to gcc20
    if (this->map_resources.find (key) == this->map_resources.end ()) //texture was never loaded before
    {
        switch (type)
        {
            case resource_type::SHADER:
                ret = new Shader ();
                break;
            //end case resource_type::SHADER:

            case resource_type::CAMERA:
                ret = new Camera2D ();
                break;
            //end case resource_type::CAMERA:

            case resource_type::TEXTURE:
                ret = new Texture (key);
                break;
            //end case resource_type::TEXTURE:

            case resource_type::FONT:
                // ret = new Font (key); Fonts require a size, so we can't pre load the target file!!!
                ret = new Font ();
                break;
            //end case resource_type::FONT:

            case resource_type::MUSIC:
                ret = new UndecodedFileSource (key);
                break;
            //end case resource_type::MUSIC:

            case resource_type::SOUND_EFFECT:
                ret = new DecodedFileSource (key);
                break;
            //end case resource_type::SOUND_EFFECT:

            default:
                throw std::invalid_argument (fmt::format ("{0}:{1} - type {2} requested is unknown!", __LINE__, __FILE__, type));
                break;
        }//end switch (type)

                                                      //count
        this->map_resources.emplace (key, resource_slot{0, type, ret});
        //count will be incremented on next step if necessary
    }//end it (!ret)
    else
        ret = this->map_resources[key].vp_package;

    //check if the domain has a reference to it.
    //a domain may ask for a resource as many times as it needs, no extra references should be added
    if (!this->_resourceForDomainExist (id, key))
    {
        this->map_domains.emplace (id, key);

        //check overflow
        if (this->map_resources[key].u_count + 1 < this->map_resources[key].u_count)
            throw std::overflow_error (fmt::format ("{0}:{1} - resource count reference overflown for {2}!", __LINE__, __FILE__, key));

        ++this->map_resources[key].u_count;
    }//end if (!this->_resourceForDomainExist (id, key))

    return ret;
}//End _getResource ()


//check if a domain already uses the resource or not
bool ResourceManager::_resourceForDomainExist (DomainID_t id, const std::string &key) const
{
    bool ret = false;
    auto range = this->map_domains.equal_range (id);

    for (auto itr = range.first; itr != range.second; ++itr)
    {
        if (itr->second == key)
        {
            ret = true;
            break;
        }//end if (itr->second.key == key)
    }//end for (auto itr = range.first; itr != range.second; ++itr)

    return ret;
}//end _resourceForDomainExist ()


/**
 * @brief Loads and compiles (if needed) the basic shader programs offered by this library.
 *
 * It will also set the state of the required uniforms to neutral values on the first load, like a orthographic
 *    projection with the current window size as parameter.
 *
 * @param id - the domain id to be associated with this resource.
 *
 * @return A pointer to the desired Shader.
 *
 * @throws std::runtime_error - in case something went wrong wile compiling/linking the shaders and program.
 * @throws std::overflow_error - in case the reference counter overflows and we can´t add more to domains (it should never happen though).
 */
Shader* ResourceManager::getBasicShader (DomainID_t id)
{
    Shader *basic = this->getShader (id, BASIC_SHADER_KEY);

    _debug (TRACE, "::%s (id=%u)", __FUNCTION__, id);

    //check if this is the first time loading this program and compile shaders if needed
    if (!basic->isCompiled ())
        basic->compile (vert_shader.c_str (), frag_shader.c_str ());

    return basic;
}//End getBasicShader ()


/**
 * @brief Gets the desired Shader from the manager.
 * @remark You are responsible for loading and compiling the vertex/fragment sources after the first time requesting for a Shader.
 *
 * @param id - the domain id to be associated with this resource.
 * @param program_name - a name to use as key to store and retrieve the Shader.
 *
 * @return A pointer to the desired Shader.
 *
 * @throws std::overflow_error - in case the reference counter overflows and we can´t add more to domains (it should never happen though).
 */
Shader* ResourceManager::getShader (DomainID_t id, const char *program_name)
{
    _debug (TRACE, "::%s (id=%u, program=%s)", __FUNCTION__, id, program_name);

    return (Shader *) this->_getResource (id, program_name, resource_type::SHADER);
}//End getShader ()


/**
 * @brief Loads a default camera view to be shared throughout the game.
 *
 * @param id - the domain id to be associated with this resource.
 *
 * @return A pointer to the default camera.
 *
 * @throws std::overflow_error - in case the reference counter overflows and we can´t add more to domains (it should never happen though).
 */
Camera2D* ResourceManager::getDefaultCamera (DomainID_t id)
{
    _debug (TRACE, "::%s (id=%u)", __FUNCTION__, id);

    return (Camera2D *) this->_getResource (id, DEFAULT_CAMERA_KEY, resource_type::CAMERA);
}//End getDefaultCamera ()


/**
 * @brief Gets the desired texture from an image file.
 *
 * @param id - the domain id to be associated with this resource.
 * @param file_path - the image file to be loaded as Texture.
 *
 * @return A pointer to the desired Texture.
 *
 * @throws std::invalid_argument - in case file_name is null.
 * @throws std::runtime_error - in case there is any problem loading the image file.
 * @throws std::overflow_error - in case the reference counter overflows and we can´t add more to domains (it should never happen though).
 */
Texture* ResourceManager::getTexture (DomainID_t id, const char *file_path)
{
    _debug (TRACE, "::%s (id=%u, file=%s)", __FUNCTION__, id, file_path);

    return (Texture *) this->_getResource (id, file_path, resource_type::TEXTURE);
}//End getTexture ()

/**
 * @brief Gets the desired character atlas from an font file.
 * @remarks Because of limitations on indexing, there can only be one instance of a font at any given time.<br>
 *          That means you should aim for an intermediate font size and let OpenGl do its thing to scale it.
 *
 * @param id - the domain id to be associated with this resource.
 * @param file_path - the font file to be loaded as character atlas.
 * @param glyph_height - the size in pixels to render the font.
 *
 * @return A pointer to the desired font as a texture.
 *
 * @throws std::invalid_argument - in case file_name is null.
 * @throws std::runtime_error - in case there is any problem loading the file.
 * @throws std::overflow_error - in case the reference counter overflows and we can´t add more to domains (it should never happen though).
 */
Font* ResourceManager::getFont (DomainID_t id, const char *file_path, unsigned glyph_height)
{
    Font *aux = nullptr;

    _debug (TRACE, "::%s (id=%u, file=%s)", __FUNCTION__, id, file_path);

    aux = (Font *) this->_getResource (id, file_path, resource_type::FONT);
    if (!aux->loaded ())
        aux->load (file_path, glyph_height);

    return aux;
}//End getFont ()


/**
 * @brief Gets the desired music from an audio file.
 *
 * @param id - the domain id to be associated with this resource.
 * @param file_path - the audio file path to be loaded as UndecodedFileSource.
 *
 * @return A pointer to the desired UndecodedFileSource.
 *
 * @throws std::invalid_argument - in case file_name is null.
 * @throws std::runtime_error - in case there is any problem loading the image file.
 * @throws std::overflow_error - in case the reference counter overflows and we can´t add more to domains (it should never happen though).
 */
UndecodedFileSource* ResourceManager::getMusic (DomainID_t id, const char *file_path)
{
    _debug (TRACE, "::%s (id=%u, file=%s)", __FUNCTION__, id, file_path);

    return (UndecodedFileSource *) this->_getResource (id, file_path, resource_type::MUSIC);
}//End getMusic ()


/**
 * @brief Gets the desired sound effect from an audio file.
 *
 * @param id - the domain id to be associated with this resource.
 * @param file_path - the audio file path to be loaded as DecodedFileSource.
 *
 * @return A pointer to the desired DecodedFileSource.
 *
 * @throws std::invalid_argument - in case file_name is null.
 * @throws std::runtime_error - in case there is any problem loading the image file.
 * @throws std::overflow_error - in case the reference counter overflows and we can´t add more to domains (it should never happen though).
 */
DecodedFileSource* ResourceManager::getSoundEffect  (DomainID_t id, const char *file_path)
{
    _debug (TRACE, "::%s (id=%u, file=%s)", __FUNCTION__, id, file_path);

    return (DecodedFileSource *) this->_getResource (id, file_path, resource_type::SOUND_EFFECT);
}//End getSoundEffect ()

